package com.mattgarner.jaddas.node;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;

import com.mattgarner.jaddas.common.util.ByteConverter;
import com.mattgarner.jaddas.node.data.PrimaryKeyCreator;
import com.mattgarner.jaddas.node.data.ResultPair;
import com.sleepycat.je.Cursor;
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.LockMode;
import com.sleepycat.je.OperationStatus;
import com.sleepycat.je.SecondaryConfig;
import com.sleepycat.je.SecondaryDatabase;

public class LocalDataProvider {
	private static LocalDataProvider instance;
    private NodeConfigurationManager nodeConfig;
    private HashMap<String, Environment> bdbEnvinronments;
	private ErrorLogManager logManager;
	
	public LocalDataProvider() {
		nodeConfig = NodeConfigurationManager.getInstance();
		logManager = ErrorLogManager.getInstance();
		bdbEnvinronments = new HashMap<String, Environment>();
		
		// pre-load the environments
		EnvironmentConfig envConfig = new EnvironmentConfig();
	    envConfig.setAllowCreate(true);
	    try {
	    	bdbEnvinronments.put(nodeConfig.getSystemDataStore(), new Environment(new File(nodeConfig.getSystemDataStore()), envConfig));
	    	bdbEnvinronments.put(nodeConfig.getIndexDataStore(), new Environment(new File(nodeConfig.getIndexDataStore()), envConfig));
	    	bdbEnvinronments.put(nodeConfig.getDataDataStore(), new Environment(new File(nodeConfig.getDataDataStore()), envConfig));
	    } catch(Exception e) {
	    	logManager.writeToLog(1, "BDB", "Unable to open DBD database environment: "+e.getMessage());
	    }
	}
	
	// singleton method
	public final static LocalDataProvider getInstance() {
		if(instance == null) { 
			instance = new LocalDataProvider();
		}
		return instance;
	}
	
	// was synchronized (but not necessary?)
	public final Database openDatabase(String dataStore, String dataName, boolean createNewDatabase) {	
		EnvironmentConfig envConfig = new EnvironmentConfig();
	    envConfig.setAllowCreate(true);
	    Environment bdbEnv = bdbEnvinronments.get(dataStore);
	    try {
	    	// Open the database. Create it if it does not already exist.
	        DatabaseConfig dbConfig = new DatabaseConfig();
	        dbConfig.setAllowCreate(createNewDatabase);
	        dbConfig.setDeferredWrite(true);
	        Database dbHandle = bdbEnv.openDatabase(null, dataName, dbConfig);
	        return dbHandle;
	    } catch(DatabaseException e) {
	    	logManager.writeToLog(1, "BDB", "Unable to open DBD database: " + e.getMessage());
	    }
	    return null;
	}
	
	// alternate signature
	public Database openDatabase(String dataStore, String dataName) {
		return openDatabase(dataStore, dataName, false);
	}
	
	// we use the BDB concept of a secondary database (really an alternate BTREE index
	// on the primary database) to hold the table-specified primary key for each tuple.
	// This allows a speedy, direct lookup on a prikey. In contrast, the key used
	// on the primary database is the unique, sequential, tuple ID that corresponds
	// with the bitmap indexes.
	public final SecondaryDatabase openSecondaryDatabase(Database dbHandlePrimary) {
		try {
			Environment dbdEnv = dbHandlePrimary.getEnvironment();
			DatabaseConfig dbConfigPrimary = dbHandlePrimary.getConfig();
			SecondaryConfig dbConfigSecondary = new SecondaryConfig();
			
			// some secondary attributes
			dbConfigSecondary.setAllowPopulate(true);
			
			// copy some attributes from the primary database config
			dbConfigSecondary.setAllowCreate(dbConfigPrimary.getAllowCreate());
			dbConfigSecondary.setReadOnly(dbConfigPrimary.getReadOnly());
			dbConfigSecondary.setDeferredWrite(dbConfigPrimary.getDeferredWrite());
			
			PrimaryKeyCreator priKeySelector = new PrimaryKeyCreator();
			dbConfigSecondary.setKeyCreator(priKeySelector);
			
			return dbdEnv.openSecondaryDatabase(null, dbHandlePrimary.getDatabaseName() + "_prikey", dbHandlePrimary, dbConfigSecondary);
	    } catch(DatabaseException e) {
	    	logManager.writeToLog(1, "BDB", "Unable to open secondary DBD database: " + e.getMessage());
	    }
	    return null;		
	}
	
	public final synchronized void closeDatabase(Database dbHandle) {
		String dbName = "";
		try { 
			dbName = dbHandle.getDatabaseName();
			dbHandle.close();
		} catch(DatabaseException e) {
			logManager.writeToLog(1, "BDB", "Unable to close DBD database at '"+dbName+"': "+e.getMessage());
		}
		
	}
	
	public final void put(Database dbHandle, byte[] key, byte[] value) throws DatabaseException {
		dbHandle.put(null, new DatabaseEntry(key), new DatabaseEntry(value));
		System.out.println("put: key: " + key.length + " data: " + value.length);
	}
	
	public final OperationStatus put(Database dbHandle, int key, byte[] value) throws DatabaseException {
		try {
			byte[] oKey = ByteConverter.convertToBytes(key);
			OperationStatus oStatus = dbHandle.put(null, new DatabaseEntry(oKey), new DatabaseEntry(value));
			
			if(oStatus == OperationStatus.SUCCESS) {
				System.out.println("put: key: " + oKey.length + " data: " + value.length);
			}
		
			return oStatus;
		} catch(IOException e) {
			throw new DatabaseException("IOException: " + e.getMessage());
		}
	}
	
	public final void put(Database dbHandle, int key, String value) throws DatabaseException {
		try {
			if(value == null) {
				value = "" + '\000'; // null marker
			}
			put(dbHandle, key, ByteConverter.convertToBytes(value));
		} catch(IOException e) {
			throw new DatabaseException("IOException: " + e.getMessage());
		}
	}
	
	public final void put(Database dbHandle, String key, byte[] value) throws DatabaseException {
		try {
			dbHandle.put(null, new DatabaseEntry(ByteConverter.convertToBytes(key)), new DatabaseEntry(value));
		} catch(IOException e) {
			throw new DatabaseException("IOException: " + e.getMessage());
		}
	}
	
	public final void put(Database dbHandle, String key, String value) throws DatabaseException {
		if(value == null) {
			value = "" + '\000'; // null marker
		}
		try {
			put(dbHandle, key, ByteConverter.convertToBytes(key));
		} catch(IOException e) {
			throw new DatabaseException("IOException: " + e.getMessage());
		}
	}
	
	
	public final String getString(Database dbHandle, String key, LockMode lMode) throws DatabaseException {
		try {
			DatabaseEntry dbData = new DatabaseEntry();
			dbHandle.get(null, new DatabaseEntry(ByteConverter.convertToBytes(key)), dbData, lMode);
			if(dbData.getData() == null) {
				return null;
			}
			return ByteConverter.convertToString(dbData.getData());
		} catch(IOException e) {
			throw new DatabaseException("IOException: " + e.getMessage());
		}
	}
	
	public final String getString(Database dbHandle, int key, LockMode lMode) throws DatabaseException {
		try {
			DatabaseEntry dbData = new DatabaseEntry();
			dbHandle.get(null, new DatabaseEntry(ByteConverter.convertToBytes(key)), dbData, lMode);
			if(dbData.getData() == null) {
				return null;
			}
			return ByteConverter.convertToString(dbData.getData());
		} catch(IOException e) {
			throw new DatabaseException("IOException: " + e.getMessage());
		}
	}
	
	public final byte[] getBytes(Database dbHandle, String key, LockMode lMode) throws DatabaseException {
		try {
			DatabaseEntry dbData = new DatabaseEntry();
			dbHandle.get(null, new DatabaseEntry(ByteConverter.convertToBytes(key)), dbData, lMode);
			return dbData.getData();
		} catch(IOException e) {
			throw new DatabaseException("IOException: " + e.getMessage());
		}
	}
	
	public final byte[] getBytes(Database dbHandle, int key, LockMode lMode) throws DatabaseException {
		try {
			DatabaseEntry dbData = new DatabaseEntry();
			dbHandle.get(null, new DatabaseEntry(ByteConverter.convertToBytes(key)), dbData, lMode);
			return dbData.getData();
		} catch(IOException e) {
			throw new DatabaseException("IOException: " + e.getMessage());
		}
	}
	
	public final byte[] getBytes(Database dbHandle, long key, LockMode lMode) throws DatabaseException {
		try {
			DatabaseEntry dbData = new DatabaseEntry();
			dbHandle.get(null, new DatabaseEntry(ByteConverter.convertToBytes(key)), dbData, lMode);
			return dbData.getData();
		} catch(IOException e) {
			throw new DatabaseException("IOException: " + e.getMessage());
		}
	}

	public final byte[] getBytes(Database dbHandle, byte[] key, LockMode lMode) throws DatabaseException {
		DatabaseEntry dbData = new DatabaseEntry();
		dbHandle.get(null, new DatabaseEntry(key), dbData, lMode);
		return dbData.getData();
	}
	
	public final ResultPair getResultPair(Database dbHandle, byte[] key, LockMode lMode) throws DatabaseException {
		DatabaseEntry dbKey = new DatabaseEntry(key);
		DatabaseEntry dbData = new DatabaseEntry();
		dbHandle.get(null, dbKey, dbData, lMode);
		return new ResultPair(dbKey.getData(), dbData.getData());		
	}
	
	public final ResultPair getResultPair(SecondaryDatabase dbHandle, byte[] key, LockMode lMode) throws DatabaseException {
		DatabaseEntry dbKey = new DatabaseEntry(key);
		DatabaseEntry dbPKey = new DatabaseEntry();
		DatabaseEntry dbData = new DatabaseEntry();
		dbHandle.get(null, dbKey, dbPKey, dbData, lMode);
		return new ResultPair(dbPKey.getData(), dbData.getData());
	}
	
	
	
	
	public final void remove(Database dbHandle, int key) throws DatabaseException {
		try {
			dbHandle.delete(null, new DatabaseEntry(ByteConverter.convertToBytes(key)));
		} catch(IOException e) {
			throw new DatabaseException("IOException: " + e.getMessage());
		}
	}
	
	public final void remove(Database dbHandle, String key) throws DatabaseException {
		try {
			dbHandle.delete(null, new DatabaseEntry(ByteConverter.convertToBytes(key)));
		} catch(IOException e) {
			throw new DatabaseException("IOException: " + e.getMessage());
		}
	}
	
	public final int removeAll(Database dbHandle) throws DatabaseException {
		// TODO Replace with proper database method
		
		Cursor cursor = dbHandle.openCursor(null, null);
		DatabaseEntry key = new DatabaseEntry();
		DatabaseEntry data = new DatabaseEntry();
		
		int rowCounter = 0;
		while(cursor.getNext(key, data, LockMode.DEFAULT) == OperationStatus.SUCCESS) {
			dbHandle.delete(null, key);
			rowCounter++;
		}
		cursor.close();
		
		return rowCounter;
	}
	
	public final void syncToDisk(Database dbHandle) throws DatabaseException {
		dbHandle.sync();
		dbHandle.getEnvironment().sync();
	}
	
	public final ResultPair[] getAllValues(Database dbHandle) throws DatabaseException {
		return getAllValues(dbHandle, 0);
	}
	
	public final ResultPair[] getAllValues(Database dbHandle, int limit) throws DatabaseException {
		Cursor cursor = dbHandle.openCursor(null, null);
		
		int resultCounter = 0;
		ArrayList<ResultPair> output = new ArrayList<ResultPair>();
		DatabaseEntry key = new DatabaseEntry();
		DatabaseEntry data = new DatabaseEntry();
		
		while(cursor.getNext(key, data, LockMode.DEFAULT) == OperationStatus.SUCCESS) {
			output.add(new ResultPair(key.getData(), data.getData()));
			resultCounter++;
			if((limit > 0) && (resultCounter == limit)) {
				break;
			}
		}
		cursor.close();
		
		return output.toArray(new ResultPair[0]);
	}
	
	public final void closeEnv(String dataStore) throws DatabaseException {
		bdbEnvinronments.get(dataStore).close();
	}
	
	///
	public final void writeToSysLog(Database dbHandle, String message) {
		try { 
			logManager.writeToLog(1, "BDB", dbHandle.getDatabaseName()+": "+message);
		} catch (DatabaseException e){
			logManager.writeToLog(1, "BDB", "UNKNOWN: "+message);
		}
	}

	
}
